package il.technion.cs236369.webserver;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.nio.file.AccessDeniedException;
import java.util.LinkedList;
import java.util.Queue;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;

public class SocketReader extends Thread
{
	// each thread receives a serial number - for debug purposes
	private int threadNum = 0;
	// holds the socket queue
	private Queue<Socket> sockets = new LinkedList<Socket>();
	// holds the request queue
	private Queue<ExtendedHttpRequest> requests = new LinkedList<ExtendedHttpRequest>();
	private int requestsCapacity = -1;
	// hold the current socket being handled by the socket reader 
	private Socket currentSocket = null;
	// used for binding to the sockets
	private HttpParams params;
	private String baseDir = null;
	private String welcomeFile = null;

	/**
	 * Start the socket reader thread with access to the sockets
	 * and requests queue, binding parameters and a new thread number
	 * @param socketQueue the general socket queue
	 * @param requestsQueue the general request queue
	 * @param requestsCapacity the possible capacity of the requests queue
	 * @param params the socket binding parameters
	 * @param baseDir the path to the website base dir
	 * @param welcomeFile the name of the welcome file
	 * @param ThreadNumber a unique identifying thread number
	 */
	public SocketReader(Queue<Socket> socketQueue, 
						Queue<ExtendedHttpRequest> requestsQueue, int requestsCapacity,  
						HttpParams params, String baseDir, String welcomeFile,  
						int ThreadNumber)
	{
		this.threadNum = ThreadNumber;
		this.sockets = socketQueue;
		this.requests = requestsQueue;
		this.requestsCapacity = requestsCapacity;
		this.params = params;
		this.baseDir = baseDir;
		this.welcomeFile = welcomeFile;
	}

	/**
	 * listen to the socket queue, pops a socket and extract a request from it
	 */
	@Override
	public void run()
	{
		ExtendedHttpRequest request = null;
		while (true)
		{
			this.currentSocket = null;
			//get socket from queue
			getSocket();

			try
			{
				//parse request
				request = getRequestFromSocket();
				if (request != null)
				{
					//if parsed request successfully puts request into the queue
					synchronized (requests)
					{
						if (requests.size() < this.requestsCapacity)
						{
							requests.add(request);
							requests.notify();
						} 
						else
						{
							System.err.println("Error: Request queue is full");
							HttpResponser responsd = new HttpResponser();
							responsd.sendErrorResponse(request.GetSocket(), 
														503, "Service Unavailable",
														"Server is temporarily unavailable, please try again later");
						}
					} // end sync requests
				} // end request != null 
				else
				{
					// no need to keep socket open for the request handler, 
					// close the connection
					if (currentSocket != null)
					{
						try
						{
							currentSocket.close();
						} catch (IOException e)
						{
							System.err.println("Error: Can't close socket " + 
												currentSocket.toString() );
						}
					}
				} // end request == null
			}
			catch (Exception e)
			{
				System.err.println(e.getStackTrace());
				System.err.println(e.getCause());
				e.printStackTrace();
			}
		}
	}

	/**
	 * creates an http request from the current handled socket
	 * @return HttpRequest (Class defined in this project)
	 */
	private ExtendedHttpRequest getRequestFromSocket()
	{
		if (currentSocket == null)
		{
			return null;
		}

		DefaultHttpServerConnection serverConn = new DefaultHttpServerConnection();
		ExtendedHttpRequest request = null;
		
		try
		{
			serverConn.bind(this.currentSocket, this.params);

			//set time out for each connection to 45 seconds
			serverConn.setSocketTimeout(45000);
		} 
		catch(IOException e)
		{
			System.err.println("SocketReader.getRequestFromSocket: Unable to bind to client");
			try
			{
				serverConn.close();
			} catch (IOException e1)
			{
				System.err.println("SocketReader.getRequestFromSocket: Can not close server connection.");
				e1.printStackTrace();
			}
			return null;
		}
		
		try
		{
			// Receive connection request
			HttpRequest requestHeaders = serverConn.receiveRequestHeader();
			String methodType = requestHeaders.getRequestLine().getMethod();
			if (methodType.contains("GET"))
			{
				request = ParseGetRequest(serverConn, requestHeaders);
				return request;
			} 
			else
			{
				request = ParsePostRequest(serverConn, requestHeaders); 
				return request; 
			}
		}
		catch (AccessDeniedException e)
		{
			HttpResponser responsd = new HttpResponser();
			responsd.sendErrorResponse(currentSocket, 403,"Forbidden", e.getMessage().substring(e.getMessage().indexOf(": ")));
			System.err.println(e.getMessage());
			return null;
		}
		catch (FileNotFoundException e)
		{
			HttpResponser responsd = new HttpResponser();
			responsd.sendErrorResponse(currentSocket, 404,"Not Found", e.getMessage().substring(e.getMessage().indexOf(": ")));
			System.err.println(e.getMessage());
			return null;
		}
		//in case of errors closes the connection and return null
		catch (IOException e)
		{
			System.err.print("IO Error: ");
			System.err.println(e.getMessage());
			e.printStackTrace(System.err);
			HttpResponser respond = new HttpResponser();
			respond.sendErrorResponse(currentSocket,400, "Bad Request", "Cannot create connection");
			return null;
		}
		catch (HttpException e)
		{
			System.err.print("HTTP Error: ");
			e.printStackTrace();	        		
			HttpResponser respond = new HttpResponser();
			respond.sendErrorResponse(currentSocket,400, "Bad Request", "Cannot create connection");
			return null;
		} 
		finally
		{
			try
			{
				//in case of a failure, no need to keep to connection option, close the connection
				if ( request  == null)
				{
					serverConn.close();
					System.err.println("SocketReader.getRequestFromSocket: Connection Closed - Request is Empty");
				}
			} 
			catch (IOException e)
			{
				System.err.println("SocketReader.getRequestFromSocket: IO Error - " + e.getMessage());
				e.printStackTrace();
			}
		}
	}

	/**
	 * Extracts parameters from an httpRequest body and puts into a ExtendedHttpRequest
	 * @param serverConn - the active server connection
	 * @param httpRequest - the basic request
	 * @param request - the extended request to attach to
	 * @return
	 */
	private ExtendedHttpRequest ExtractParamsFromBody(DefaultHttpServerConnection serverConn, 
														HttpRequest httpRequest, 
														ExtendedHttpRequest request) 
	{
		try 
		{
			//to get the body of the request need to wrap the http request
			HttpEntityEnclosingRequest enclosingRequest = (HttpEntityEnclosingRequest) httpRequest;
			serverConn.receiveRequestEntity(enclosingRequest);
			HttpEntity entity = enclosingRequest.getEntity();
			// using a reader to read the body according to the UTF-8 encoding
			Reader contentReader = new BufferedReader(new InputStreamReader(entity.getContent(),"UTF-8"));
			
			StringBuilder parsedContent = new StringBuilder();
			int numChar = 0;
			
			while ((numChar = contentReader.read()) != -1)
			{
				parsedContent.append(Character.toChars(numChar));
			}
			contentReader.close();
			EntityUtils.consume(entity);
			
			//Takes the body content (after parsing) and extracts all parameters
			AddParamatersToRequest(parsedContent.toString(),request);
			return request;
		} 
		catch (IOException | HttpException e) 
		{
			HttpResponser respond = new HttpResponser();
			respond.sendErrorResponse(currentSocket,400, "Bad Request", "connection failed to extract body");
			return null;
		}
	}
	
	/**
	/**
	 * Parses a post request
	 * @param serverConn - connection to receive the request body
	 * @param httpRequest - contains headers 
	 * @return
	 * @throws FileNotFoundException 
	 * @throws AccessDeniedException 
	 */
	private ExtendedHttpRequest ParsePostRequest(DefaultHttpServerConnection serverConn,
													HttpRequest httpRequest)
	{
		// creates a few of the fields of the ExtendedHttpRequest
		ExtendedHttpRequest request = new ExtendedHttpRequest(currentSocket, 
																serverConn, 
																httpRequest);
		request.SetTypeToPostResponse();
		String path = "/";
		
		try 
		{
			path = ExtractUrlFromString(httpRequest);
		}
		catch (AccessDeniedException | FileNotFoundException e) 
		{
			e.printStackTrace();
		} 

		request.SetPath(path);
		//get the parameters of the request - adds parameters to the ExtendedHttpRequest
		request = ExtractParamsFromBody(serverConn, httpRequest, request);
		return request;
		
	}

	/**
	 * creates an extended http request from a get request
	 * @param serverConn
	 * @param httpRequest
	 * @return
	 */
	private ExtendedHttpRequest ParseGetRequest(DefaultHttpServerConnection serverConn,
												HttpRequest httpRequest) 
												throws AccessDeniedException, 
														FileNotFoundException
	{
		ExtendedHttpRequest request = new ExtendedHttpRequest(currentSocket,
																serverConn,
																httpRequest);
		
		// if this isn't a servlet, process like a page
//		if (WebServer.map.getServletType(Mappings.getServletPath(request)) == null)
//		{
			request.SetTypeToGetResponse();
			String path = ExtractUrlFromString(httpRequest);
			request.SetPath(path);
			if (httpRequest.getRequestLine().getUri().split("[?]").length > 1)
			{
				String parameters = httpRequest.getRequestLine().getUri().split("[?]")[1];
				AddParamatersToRequest(parameters,request);
			}
//		}
		return request;
	}

	private String ExtractUrlFromString(HttpRequest httpRequest) 
										throws AccessDeniedException, 
												FileNotFoundException
	{
		String path = httpRequest.getRequestLine().getUri().split("[?]")[0];
		String basedirNoEnding = "";
		//in the case the url is absolute, test the the user refers to the root directory
		if (path.contains(":"))
		{
			path = path.substring(1,path.length());
			path = path.replace("/", "\\");
			basedirNoEnding = this.baseDir.substring(0,this.baseDir.length()-2);
			if (!path.startsWith(basedirNoEnding))
				throw new AccessDeniedException("Access is allowed to files under " + 
												this.baseDir + 
												"direcotry only\n <br/> \"" + 
												path + "\" is illegal");
		}		
		else if (WebServer.map.getServletType(path) == null)
		{
			// handles relative urls
			path = path.replace("/", "\\");
			path = path.substring(1,path.length());
			path = this.baseDir + path;
		}
		else
		{
			return path;
		}
		
		//if user calls the webserver only (localhost/8080) returns index page
		if (path.equals("") || path.equals("/") || 
			path.equals(this.baseDir) || path.equals(basedirNoEnding))
		{
			path = this.baseDir + this.welcomeFile;
		}
		
		//test whether file/direcotry exists and is readable
		File f = new File(path);
		if (!f.exists())
		{
			throw new FileNotFoundException("SocketReader.ExtractUrlFromString: " +
											"The file or directory do not exist");
		}
		if (!f.canRead())
		{
			throw new AccessDeniedException("SocketReader.ExtractUrlFromString: " + 
											"Can't read files - you do not have premission");
		}
		
		//The can read didn't work on files so this part fixes it
		if (f.isFile())
		{
			try
			{
				BufferedReader reader = new BufferedReader( new FileReader (f));
				reader.close();
			} 
			catch (FileNotFoundException e)
			{
				throw new AccessDeniedException("SocketReader.ExtractUrlFromString: " +
												"Can't read files - you do not have premission");
			} catch (IOException e)
			{
				throw new AccessDeniedException("SocketReader.ExtractUrlFromString: " +
												"Can't read files - you do not have premission");
			}
		}
		
		return path;
	}
	
	/**
	 * receives an ExtendedHttpRequest and a string containing request 
	 * 	parameters and parses the parameters into the request
	 * @param parameters - parameters accompanying the request
	 * @param request - the request sent
	 * @return an ExtendedHttpRequest with all the parameters
	 */
	private ExtendedHttpRequest AddParamatersToRequest(String parameters, 
														ExtendedHttpRequest request) 
	{
		if (parameters == null)
		{
			return request;
		}
		
		String[] brokenParamaters = parameters.split("[&]");
		
		for (String parameter : brokenParamaters)
		{
			try
			{
				if (parameter.split("[=]").length >= 2)
				{
					String field = java.net.URLDecoder.decode(parameter.split("[=]")[0],"UTF-8");
					String value = java.net.URLDecoder.decode(parameter.split("[=]")[1],"UTF-8");
					System.out.println("SocketReader.AddParametersToRequest: " + field + ": " + value);
					request.SetParameter(field,value);
				}
			}
			catch (UnsupportedEncodingException e) 
			{
				HttpResponser respond = new HttpResponser();
				respond.sendErrorResponse(currentSocket, 400, 
											"Bad Request", "Illegal enconding!");
				return request;
			}
		}
		
		return request;
	}

	/**
	 * listen to the sockets queue, when not empty retrieves a socket and removes it from queue
	 */
	private void getSocket() 
	{
		synchronized (this.sockets) 
		{
			while (this.sockets.isEmpty()) 
			{
				try 
				{
					this.sockets.wait();
				} catch (InterruptedException | IllegalMonitorStateException e) 
				{
					System.err.println(this.threadNum + " was interrupted");
					e.printStackTrace();
				}
			}
			this.currentSocket = this.sockets.remove();
			//System.err.println(ThreadNum + " was interrupted");
		}
	}
}
